/*Scala code license:

 Copyright 2009/10 Osman Pub

 Licensed under the Apache License, Version 2.0 (the "License") you may not use
 this file except in compliance with the License.

 You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software distributed
 under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 CONDITIONS OF ANY KIND, either express or implied. See the License for the
 specific language governing permissions and limitations under the License.

 */
/**
 Orginal java code credits:
 * @author ???
 */
/**
 Addendum: OP:
 Note no sound implemented since I'm only interested in the OpenGL code
 Also the text has some kind of mirroring problems
 */

package demos

import com.sun.opengl.util.BufferUtil
import common._
import java.awt.event._
import javax.media.opengl._
import javax.swing._
import java.util.Random

object Lesson21 {
  def main(args: Array[String]) {
    val glDisplay = GLDisplay("Lesson 21: Lines, timing")
    glDisplay addGLEventListener(new Renderer)
    glDisplay addKeyListener(new KeyAdapter {
      override def keyPressed(e: KeyEvent) =
        response = e getKeyCode match {
          case KeyEvent.VK_UP => Action up
          case KeyEvent.VK_DOWN => Action down
          case KeyEvent.VK_RIGHT => Action right
          case KeyEvent.VK_LEFT => Action left
          case _ => Action none
        }

      override def keyReleased(e: KeyEvent) =
        e getKeyCode match {
          case KeyEvent.VK_ESCAPE => glDisplay stop
          case KeyEvent.VK_SPACE => response = Action reset
          case _ => response = Action none
        }
    })
    glDisplay start
  }

  object Action extends Enumeration { val none, up, down, left, right,
                                     reset = Value }
  private var response = Action none
  
  private val enemy = new Array[GameObject](9)
  private val hourglass = new GameObject
  private val player = new GameObject
  private val random = new Random
  // Stepping Values For Slow Video Adjustment
  private val steps =  Array(1, 2, 4, 5, 10, 20)	
  private val vline, hline = Array.ofDim[Boolean](11, 11)

  private var (adjust, base, lives) = (3, 0, 5)
  private var filled, gameover, reset  = false
  private var delay: Float = 0
  private var level, level2, stage = 1

  for(i <- 0 until 9) enemy(i) = new GameObject
  resetObjects							

  private def resetObjects {
    player x = 0; player y = 0; player fx = 0; player fy = 0
    for(i <- 0 until (stage * level)) {				
      enemy(i) = new GameObject
      enemy(i) x = 5 + (scala.math.random * 6).asInstanceOf[Int]
      enemy(i) y = (scala.math.random * 11).asInstanceOf[Int]
      enemy(i) fx = enemy(i).x * 60; enemy(i) fy = enemy(i).y * 40
    }
  }

  private def update {
    if(!gameover) {
      for(i <- 0 until stage * level) {
        if(enemy(i).x < player.x && enemy(i).fy == enemy(i).y * 40)
          enemy(i).x += 1						// Move The Enemy Right
        if(enemy(i).x > player.x && enemy(i).fy == enemy(i).y * 40)
          enemy(i).x -= 1						// Move The Enemy Left
        if(enemy(i).y < player.y && enemy(i).fx == enemy(i).x * 60)
          enemy(i).y += 1						// Move The Enemy Down
        if(enemy(i).y > player.y && enemy(i).fx == enemy(i).x * 60)
          enemy(i).y -= 1						// Move The Enemy Up

        if(delay > (3 - level) && hourglass.fx != 2) {
          delay = 0								
          for(j <- 0 until stage * level)	{
            if(enemy(j).fx < enemy(j).x * 60)	{
              enemy(j).fx += steps(adjust); enemy(j).spin += steps(adjust)
            } else {
              enemy(j).fx -= steps(adjust); enemy(j).spin -= steps(adjust)
            }
            if(enemy(j).fy < enemy(j).y * 40) {
              enemy(j).fy += steps(adjust); enemy(j).spin += steps(adjust)
            } else {
              enemy(j).fy -= steps(adjust); enemy(j).spin -= steps(adjust)
            }
          }
        }

        if((enemy(i).fx == player.fx) && (enemy(i).fy == player.fy)) {
          lives -= 1; resetObjects
          if(lives == 0) gameover = true
        }
      }

      if(response == Action.right && player.x < 10 && player.fx == player.x * 60
         && player.fy == player.y * 40) {
        hline(player x)(player y) = true; player.x += 1
      } else if(response == Action.left && player.x > 0 &&
                player.fx == player.x * 60 && player.fy == player.y * 40) {
        player.x -= 1; hline(player x)(player y) = true
      } else if(response == Action.down && player.y < 10 &&
                player.fx == player.x * 60 && player.fy == player.y * 40) {
        vline(player x)(player y) = true; player.y += 1
      } else if(response == Action.up && player.y > 0 &&
                player.fx == player.x * 60 && player.fy == player.y * 40) {
        player.y -= 1; vline(player x)(player y) = true
      }

      if(player.fx < player.x * 60) player.fx += steps(adjust)
      else player.fx -= steps(adjust)
      if(player.fy < player.y * 40) player.fy += steps(adjust)
      else player.fy -= steps(adjust)

    } else if(response == Action.reset) {
        gameover = false; filled = true; level = 1; level2 = 1
        stage = 0; lives = 5
    }

    if(filled) {
      stage += 1
      if(stage > 3) {						
        stage = 1; level += 1; level2 += 1
        if(level > 3) { level = 3; lives += 1 }
      }

      resetObjects
      for(x <- 0 until 10) for(y <- 0 until 10) {
        hline(x)(y) = false; vline(x)(y) = false
      }
    }

    if(player.fx == hourglass.x * 60 && player.fy == hourglass.y * 40 &&
       hourglass.fx == 1) {
      hourglass fx = 2; hourglass fy = 0
    }

    player.spin += 0.5f * steps(adjust)					
    if(player.spin > 360) player.spin -= 360

    hourglass.spin -= 0.25f * steps(adjust)				
    if(hourglass.spin < 0) hourglass.spin += 360

    hourglass.fy += steps(adjust)						
    if((hourglass.fx == 0) && (hourglass.fy > 6000 / level)) {
      hourglass x = scala.math.abs(random nextInt) % 10 + 1
      hourglass y = scala.math.abs(random nextInt) % 11
      hourglass fx = 1; hourglass fy = 0
    }

    if(hourglass.fx == 1 && hourglass.fy > 6000 / level) {
      hourglass fx = 0; hourglass fy = 0
    }

    if(hourglass.fx == 2 && hourglass.fy > 500 + 500 * level)	{
      hourglass fx = 0; hourglass fy = 0
    }

    delay +=1											
  }

  private class Renderer extends GLEventListener {
    private val textures = new Array[Int](2)
    private var stringBuffer = BufferUtil newByteBuffer(256)

    def display(glDrawable: GLAutoDrawable) {
      val gl = glDrawable getGL

      def print(x: Int, y: Int, set: Int, msg: String) {
        gl glEnable(GL GL_TEXTURE_2D)								// Enable Texture Mapping
        gl glLoadIdentity;									// Reset The Projection Matrix
        gl glTranslated(x, y, 0)			// Position The Text (0,0 - Bottom Left)
        // Choose The Font Set (0 or 1)
        gl glListBase(base - 32 + (128 * (if(set > 1) 1 else set)))						

        // If Set 0 Is Being Used Enlarge Font
        if(set == 0) gl glScalef(1.5f, 2, 1) // Enlarge Font Width And Height

        if(stringBuffer.capacity < msg.length)
          stringBuffer = BufferUtil newByteBuffer(msg length)

        stringBuffer clear; stringBuffer put(msg getBytes); stringBuffer.flip
        // Write The Text To The Screen
        gl glCallLists(msg length, GL GL_UNSIGNED_BYTE, stringBuffer)
        gl glDisable(GL GL_TEXTURE_2D)				// Disable Texture Mapping
      }

      def drawGLScene {
        gl glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT)       

        gl glBindTexture(GL GL_TEXTURE_2D, textures(0))				
        gl glColor3f(1, 0.5f, 1)								// Set Color To Purple
        print(207, 24, 0, "GRID CRAZY")	
        gl glColor3f(1, 1, 0)								// Set Color To Yellow
        print(20, 20, 1, "Level:" + level2)					// Write Actual Level Stats
        print(20, 40, 1, "Stage:" + stage)						// Write Stage Stats


        if(gameover) {											
          gl glColor3i(random.nextInt * 255, random.nextInt * 255,
                       random.nextInt * 255)		// Pick A Random Color
          print(472, 20, 1, "GAME OVER")		
          print(456, 40, 1, "PRESS SPACE")	
        }

        for(i <- 0 until lives - 1) {					
          gl glLoadIdentity;
          gl glTranslatef(490 + (i * 40), 40, 0)			// Move To The Right Of Our Title Text
          gl glRotatef(-player.spin, 0, 0, 1)				// Rotate Counter Clockwise
          gl glColor3f(0, 1, 0)							// Set Player Color To Light Green

          gl glBegin(GL GL_LINES)									// Start Drawing Our Player Using Lines
          gl glVertex2d(-5, -5)								// Top Left Of Player
          gl glVertex2d(5, 5)								// Bottom Right Of Player
          gl glVertex2d(5, -5)								// Top Right Of Player
          gl glVertex2d(-5, 5)								// Bottom Left Of Player
          gl glEnd;

          gl glRotatef(-player.spin * 0.5f, 0, 0, 1)		// Rotate Counter Clockwise
          gl glColor3f(0, 0.75f, 0)							// Set Player Color To Dark Green

          gl glBegin(GL GL_LINES)									// Start Drawing Our Player Using Lines
          gl glVertex2d(-7, 0)								// Left Center Of Player
          gl glVertex2d(7, 0)								// Right Center Of Player
          gl glVertex2d(0, -7)								// Top Center Of Player
          gl glVertex2d(0, 7)								// Bottom Center Of Player
          gl glEnd											// Done Drawing The Player
        }

        filled = true											// Set Filled To True Before Testing
        gl glLineWidth(2)										// Set Line Width For Cells To 2
        gl glDisable(GL GL_LINE_SMOOTH)								// Disable Antialiasing
        gl glLoadIdentity;

        for(x <- 0 until 11) 						// Loop From Left To Right
          for(y <- 0 until 11) {					// Loop From Top To Bottom
            gl glColor3f(0, 0.5f, 1)						// Set Line Color To Blue
            // Has The Horizontal Line Been Traced
            if(hline(x)(y)) gl glColor3f(1, 1, 1) // If So, Set Line Color To White

            if(x < 10) {								// Dont Draw To Far Right
              // If A Horizontal Line Isn't Filled
              if(!hline(x)(y)) filled = false							
              gl glBegin(GL GL_LINES)							// Start Drawing Horizontal Cell Borders
              gl glVertex2d(20 + (x * 60), 70 + (y * 40))// Left Side Of Horizontal Line
              gl glVertex2d(80 + (x * 60), 70 + (y * 40))// Right Side Of Horizontal Line
              gl glEnd									// Done Drawing Horizontal Cell Borders
            }

            gl glColor3f(0, 0.5f, 1)						// Set Line Color To Blue
            // Has The Horizontal Line Been Traced
            if(vline(x)(y)) gl glColor3f(1, 1, 1)					// If So, Set Line Color To White
            if(y < 10) {								// Dont Draw To Far Down
              // If A Verticle Line Isn't Filled
              if(!vline(x)(y)) filled = false							
              gl glBegin(GL GL_LINES)							// Start Drawing Verticle Cell Borders
              gl glVertex2d(20 + (x * 60), 70 + (y * 40))// Left Side Of Horizontal Line
              gl glVertex2d(20 + (x * 60), 110 + (y * 40))// Right Side Of Horizontal Line
              gl glEnd									// Done Drawing Verticle Cell Borders
            }

            gl glEnable(GL GL_TEXTURE_2D)						// Enable Texture Mapping
            gl glColor3f(1, 1, 1)						// Bright White Color
            gl glBindTexture(GL GL_TEXTURE_2D, textures(1))		// Select The Tile Image
            if((x < 10) && (y < 10)) {					// If In Bounds, Fill In Traced Boxes
              // Are All Sides Of The Box Traced?
              if(hline(x)(y) && hline(x)(y + 1) && vline(x)(y) && vline(x + 1)(y)) {
                gl glBegin(GL GL_QUADS)						// Draw A Textured Quad
                gl glTexCoord2f((x / 10.0f) + 0.1f, 1.0f - ((y / 10.0f)))
                gl glVertex2d(20 + (x * 60) + 59, (70 + y * 40 + 1))	// Top Right
                gl glTexCoord2f((x / 10.0f), 1.0f - ((y / 10.0f)))
                gl glVertex2d(20 + (x * 60) + 1, (70 + y * 40 + 1))	// Top Left
                gl glTexCoord2f((x / 10.0f), 1.0f - ((y / 10.0f) + 0.1f))
                gl glVertex2d(20 + (x * 60) + 1, (70 + y * 40) + 39)	// Bottom Left
                gl glTexCoord2f((x / 10.0f) + 0.1f, 1.0f - ((y / 10.0f) + 0.1f))
                gl glVertex2d(20 + (x * 60) + 59, (70 + y * 40) + 39)	// Bottom Right
                gl glEnd								// Done Texturing The Box
              }
            }
            gl glDisable(GL GL_TEXTURE_2D)						// Disable Texture Mapping

          }

        gl glLineWidth(1)										// Set The Line Width To 1
        gl glEnable(GL GL_LINE_SMOOTH)							// If So, Enable Antialiasing

        if(hourglass.fx == 1) {								// If fx=1 Draw The Hourglass
          gl glLoadIdentity;									// Reset The Modelview Matrix
          gl glTranslatef(20 + (hourglass.x * 60), 70 + (hourglass.y * 40), 0)	// Move To The Fine Hourglass Position
          gl glRotatef(hourglass spin, 0, 0, 1)			// Rotate Clockwise
          gl glColor3f(random nextFloat, random nextFloat, random nextFloat)		// Set Hourglass Color To Random Color
          gl glBegin(GL GL_LINES)									// Start Drawing Our Hourglass Using Lines
          gl glVertex2d(-5, -5)								// Top Left Of Hourglass
          gl glVertex2d(5, 5)								// Bottom Right Of Hourglass
          gl glVertex2d(5, -5)								// Top Right Of Hourglass
          gl glVertex2d(-5, 5)								// Bottom Left Of Hourglass
          gl glVertex2d(-5, 5)								// Bottom Left Of Hourglass
          gl glVertex2d(5, 5)								// Bottom Right Of Hourglass
          gl glVertex2d(-5, -5)								// Top Left Of Hourglass
          gl glVertex2d(5, -5)								// Top Right Of Hourglass
          gl glEnd											// Done Drawing The Hourglass
        }

        gl glLoadIdentity;
        gl glTranslatef(player.fx + 20, player.fy + 70, 0)		// Move To The Fine Player Position
        gl glRotatef(player spin, 0, 0, 1)					// Rotate Clockwise
        gl glColor3f(0, 1, 0)								// Set Player Color To Light Green
        gl glBegin(GL GL_LINES)										// Start Drawing Our Player Using Lines
        gl glVertex2d(-5, -5)									// Top Left Of Player
        gl glVertex2d(5, 5)									// Bottom Right Of Player
        gl glVertex2d(5, -5)									// Top Right Of Player
        gl glVertex2d(-5, 5)									// Bottom Left Of Player
        gl glEnd;

        gl glRotatef(player.spin * 0.5f, 0, 0, 1)				// Rotate Clockwise
        gl glColor3f(0, 0.75f, 0)								// Set Player Color To Dark Green
        gl glBegin(GL GL_LINES)										// Start Drawing Our Player Using Lines
        gl glVertex2d(-7, 0)									// Left Center Of Player
        gl glVertex2d(7, 0)									// Right Center Of Player
        gl glVertex2d(0, -7)									// Top Center Of Player
        gl glVertex2d(0, 7)									// Bottom Center Of Player
        gl glEnd												// Done Drawing The Player

        for(i <- 0 until stage * level) {				// Loop To Draw Enemies
          gl glLoadIdentity;
          gl glTranslatef(enemy(i).fx + 20, enemy(i).fy + 70, 0)
          gl glColor3f(1, 0.5f, 0.5f)							// Make Enemy Body Pink
          gl glBegin(GL GL_LINES)									// Start Drawing Enemy
          gl glVertex2d(0, -7)								// Top Point Of Body
          gl glVertex2d(-7, 0)								// Left Point Of Body
          gl glVertex2d(-7, 0)								// Left Point Of Body
          gl glVertex2d(0, 7)								// Bottom Point Of Body
          gl glVertex2d(0, 7)								// Bottom Point Of Body
          gl glVertex2d(7, 0)								// Right Point Of Body
          gl glVertex2d(7, 0)								// Right Point Of Body
          gl glVertex2d(0, -7)								// Top Point Of Body
          gl glEnd;
          
          gl glRotatef(enemy(i) spin, 0, 0, 1)		// Rotate The Enemy Blade
          gl glColor3f(1, 0, 0)							// Make Enemy Blade Red
          gl glBegin(GL GL_LINES)									// Start Drawing Enemy Blade
          gl glVertex2d(-7, -7)								// Top Left Of Enemy
          gl glVertex2d(7, 7)								// Bottom Right Of Enemy
          gl glVertex2d(-7, 7)								// Bottom Left Of Enemy
          gl glVertex2d(7, -7)								// Top Right Of Enemy
          gl glEnd											// Done Drawing Enemy Blade
        }
      }

      drawGLScene; update
    }

    /** Called when the display mode has been changed.  <B>!!
     * CURRENTLY UNIMPLEMENTED IN JOGL !!</B>
     * @param gLDrawable The GLAutoDrawable object.
     * @param modeChanged Indicates if the video mode has changed.
     * @param deviceChanged Indicates if the video device has changed.
     */
    def displayChanged(glDrawable: GLAutoDrawable, modeChanged: Boolean,
                       deviceChanged: Boolean) {}

    /** Called by the drawable immediately after the OpenGL context is
     * initialized for the first time. Can be used to perform one-time OpenGL
     * initialization such as setup of lights and display lists.
     * @param gLDrawable The GLAutoDrawable object.
     */
    def init(glDrawable: GLAutoDrawable) {
      val gl = glDrawable getGL

      def buildFont {
        base = gl glGenLists(256)								// Creating 256 Display Lists
        gl glBindTexture(GL GL_TEXTURE_2D, textures(0)) // Select Our Font Texture
        for(i <- 0 until 256) {
          val cx = (i & 15).asInstanceOf[Float] / 16
          val cy = (i / 16).asInstanceOf[Float] / 16

          gl glNewList(base + i, GL GL_COMPILE)				// Start Building A List
          gl glBegin(GL GL_QUADS)							// Use A Quad For Each Character
          gl glTexCoord2f(cx, 1 - cy - 0.0625f)			// Texture Coord (Bottom Left)
          gl glVertex2i(0, 0)						// Vertex Coord (Bottom Left)
          gl glTexCoord2f(cx + 0.0625f, 1 - cy - 0.0625f)	// Texture Coord (Bottom Right)
          gl glVertex2i(16, 0)						// Vertex Coord (Bottom Right)
          gl glTexCoord2f(cx + 0.0625f, 1 - cy)			// Texture Coord (Top Right)
          gl glVertex2i(16, 16)						// Vertex Coord (Top Right)
          gl glTexCoord2f(cx, 1 - cy)					// Texture Coord (Top Left)
          gl glVertex2i(0, 16)						// Vertex Coord (Top Left)
          gl glEnd;									// Done Building Our Quad (Character)
          gl glTranslated(10, 0, 0)						// Move To The Right Of The Character
          gl glEndList									// Done Building The Display List
        }													// Loop Until All 256 Are Built
      }

      def loadGLTextures {
        val tileNames = Array("demos/data/images/font.png",
                              "demos/data/images/Image.png")
        gl glGenTextures(2, textures, 0)
        for(i <- 0 until 2) {
          val textImg = TextureReader readTexture(tileNames(i))
          gl glBindTexture(GL GL_TEXTURE_2D, textures(i))
          gl glTexParameteri(GL GL_TEXTURE_2D, GL GL_TEXTURE_MIN_FILTER,
                             GL GL_LINEAR)
          gl glTexParameteri(GL GL_TEXTURE_2D, GL GL_TEXTURE_MAG_FILTER,
                             GL GL_LINEAR)
          gl glTexImage2D(GL GL_TEXTURE_2D, 0, GL GL_RGB, textImg width,
                          textImg height, 0,
                          GL GL_RGB, GL GL_UNSIGNED_BYTE, textImg pixels)
        }
      }

      // init
      loadGLTextures; buildFont

      gl glShadeModel(GL GL_SMOOTH)                            //Enables Smooth Color Shading
      gl glClearColor(0, 0, 0, 0)               //This Will Clear The Background Color To Black
      gl glClearDepth(1.0)                                  //Enables Clearing Of The Depth Buffer
      gl glEnable(GL GL_DEPTH_TEST)
      gl glDepthFunc(GL GL_LEQUAL)                             //The Type Of Depth Test To Do
      gl glHint(GL GL_LINE_SMOOTH_HINT, GL GL_NICEST)  // Really Nice Perspective Calculations
      gl glEnable(GL GL_BLEND)										// Enable Blending
      gl glBlendFunc(GL GL_SRC_ALPHA, GL GL_ONE_MINUS_SRC_ALPHA)		// Type Of Blending To Use
      gl glEnable(GL GL_TEXTURE_2D)							// Enable 2D Texture Mapping
    }

    /** Called by the drawable during the first repaint after the component has
     * been resized. The client can update the viewport and view volume of the
     * window appropriately, for example by a call to
     * GL.glViewport(int, int, int, int) note that for convenience the component
     * has already called GL.glViewport(int, int, int, int)(x, y, width, height)
     * when this method is called, so the client may not have to do anything in
     * this method.
     * @param gLDrawable The GLAutoDrawable object.
     * @param x The X Coordinate of the viewport rectangle.
     * @param y The Y coordinate of the viewport rectanble.
     * @param width The new width of the window.
     * @param height The new height of the window.
     */
    def reshape(glDrawable: GLAutoDrawable, x: Int, y: Int, width: Int,
                height: Int) {
      require(height != 0)

      val gl = glDrawable.getGL
      val h = width.asInstanceOf[Float] / height.asInstanceOf[Float]
      gl glViewport(0, 0, width, height)
      gl glMatrixMode(GL GL_PROJECTION)
      gl glLoadIdentity;
      // Create Ortho 640x480 View (0,0 At Top Left)
      gl glOrtho(0, width, height, 0, -1, 1)				
      gl glMatrixMode(GL GL_MODELVIEW)
      gl glLoadIdentity
    }
  }

  private class GameObject(var fx: Int = 0, var fy: Int = 0, var x: Int = 0,
                           var y: Int = 0, var spin: Float = 0) {}
}